
import {indicatrix} from "../util/math.js";
// import {gaEvent} from "../util/ga.js";
import {createMask} from "./mask.js";
// import {model as sessionConfig} from "../framework/sessionState.js";
import {report} from "../report.js";
import {$} from "../util/seq.js";
import {gaEvent} from "../util/ga.js";

const MAX_TASK_TIME = 150;                    // amount of time before a task yields control (millis)
const MIN_SLEEP_TIME = 10;                    // amount of time a task waits before resuming (millis)
const NULL_VECTOR = [NaN, NaN];               // undefined location outside the vector field
const HOLE_VECTOR = [NaN, 0];                 // signifies a hole in the vector field
const TRANSPARENT_BLACK = [0, 0, 0, 0];       // singleton 0 rgba

function createField(rows, mask, bounds) {
    const {xMin} = bounds;
    const field = {};

    /**
     * Copies the array [x, y, dx, dy, m] into Array 'a' starting at index i. If x or y is out of bounds,
     * then sets the dx, dy, and m values to NaN.
     *
     * @param x {Number}
     * @param y {Number}
     * @param a {Float32Array}
     * @param i {Number}
     */
    field.move = function(x, y, a, i) {
        const k = Math.round(y);
        if (0 <= k && k < rows.length) {
            const row = rows[k];
            const j = (Math.round(x) - xMin) * 3;
            if (row && 0 <= j && j < row.length) {
                a[i  ] = x;
                a[i+1] = y;
                a[i+2] = row[j  ];  // dx
                a[i+3] = row[j+1];  // dy
                a[i+4] = row[j+2];  // m
                return;
            }
        }
        a[i  ] = x;
        a[i+1] = y;
        a[i+2] = NaN;
        a[i+3] = NaN;
        a[i+4] = NaN;
    };

    /**
     * @returns {boolean} true if the field is valid at the point (x, y)
     */
    field.isDefined = function(x, y) {
        const k = Math.round(y);
        if (0 <= k && k < rows.length) {
            const row = rows[k];
            const j = (Math.round(x) - xMin) * 3;
            if (row && 0 <= j && j < row.length) {
                return !isNaN(row[j]);
            }
        }
        return false;
    };

    /**
     * @returns {boolean} true if the point (x, y) lies inside the outer boundary of the vector field, even if
     *          the vector field has a hole (is undefined) at that point, such as at an island in a field of
     *          ocean currents.
     */
    field.isInsideBoundary = function(x, y) {
        const a = new Float32Array(5);  // [x, y, dx, dy, m]
        field.move(x, y, a, 0);
        return !isNaN(a[3]);  // true if vector is defined or is HOLE_VECTOR
    };

    field.overlay = mask?.imageData;

    return field;
}

/**
 * Distort the vector using the projection indicatrix at point (x, y) and the velocity scale. The vector is modified
 * in place and returned by this function.
 */
function distort(project, λ, φ, x, y, velocityScale, vec) {
    const d = indicatrix(project, λ, φ, x, y);

    // Scale indicatrix by u and v, then add.
    const [u, v] = vec;
    vec[0] = (d[0] * u + d[2] * v) * velocityScale;
    vec[1] = (d[1] * u + d[3] * v) * velocityScale;
    return vec;
}

let failureReported = false;

export function interpolateField(
    viewboxAgent,
    globeAgent,
    rendererAgent,
    primaryLayerAgent,
    activeLayerAgent,
    progressHandle,
    hd_enabled,
    interpolation_type,
    fastoverlayAgent,
    cancel,
) {
    const globe = globeAgent.value();
    if (!globe || !rendererAgent.value()) {
        return null;
    }

    const fastoverlayResult = fastoverlayAgent.value()?.draw() ?? {pass: false};
    const useFastOverlay = fastoverlayResult.pass;
    if (fastoverlayResult.err && !failureReported) {
        // UNDONE: use first instance of error to permanently disable webgl. don't even try drawing again.
        failureReported = true;
        const msg = JSON.stringify(fastoverlayResult);
        console.log(`fastoverlay failure: ${msg}`);
        gaEvent("gl", msg);
    }

    const primaryLayer = primaryLayerAgent.value();
    const activeLayer = activeLayerAgent.value();
    const hasDistinctOverlay = primaryLayer !== activeLayer;

    // nothing to do if products failed to load and have no data
    if (!primaryLayer?.field || !activeLayer?.field) {
        return null;
    }
    // TIME-LAPSE
    // if (!primaryLayer.particles) {
    //     report.status("");
    //     return;
    // }

    const step = hd_enabled ? 1 : 2;

    const primaryField = primaryLayer.field();
    const activeField = activeLayer.field();
    const primaryInterpolate = primaryField[interpolation_type];
    const activeInterpolate = activeField[interpolation_type];
    const primaryScalarize = primaryField.scalarize;
    const activeScalarize = activeField.scalarize;

    const mask = createMask(globe, viewboxAgent);

    console.time("interpolating field");

    const {project, invert} = globe.projection.optimize();

    const bounds = globe.bounds(viewboxAgent.value());
    const {xMin, yMin, xMax, yMax, width, height} = bounds;

    // How fast particles move on the screen (arbitrary value chosen for aesthetics).
    const velocityScale = primaryLayer.particles.velocityScale;

    const rows = [];
    let y = yMin;
    const colorScale = activeLayer.scale;
    const alpha = activeLayer.alpha.animated;

    function interpolateRow(y) {
        const lastRow = y === yMax;
        const row = new Float32Array(width * 3);  // [u0, v0, m0, u1, v1, m1, ...]
        for (let x = xMin, i = 0; x <= xMax; x += step, i += step*3) {
            const isLastColumn = x === xMax;
            let vec = NULL_VECTOR;
            let mag = NaN;
            if (mask.isVisible(x, y)) {
                const [λ, φ] = invert(x, y);
                let color = TRANSPARENT_BLACK;
                if (λ === λ) {
                    vec = primaryInterpolate(λ, φ);
                    let scalar = mag = primaryScalarize(vec);
                    if (scalar === scalar) {
                        vec = distort(project, λ, φ, x, y, velocityScale, vec);
                    } else {
                        vec = HOLE_VECTOR;
                    }
                    if (!useFastOverlay) {
                        if (hasDistinctOverlay) {
                            scalar = activeScalarize(activeInterpolate(λ, φ));
                        }
                        if (scalar === scalar) {
                            color = colorScale.rgba(scalar);
                            color[3] = alpha;
                        }
                    }
                }
                if (!useFastOverlay) {
                    mask.set(x, y, color);
                    if (!hd_enabled) {
                        if (!isLastColumn) {
                            mask.set(x+1, y, color);
                            if (!lastRow) {
                                mask.set(x+1, y+1, color);
                            }
                        }
                        if (!lastRow) {
                            mask.set(x, y+1, color);
                        }
                    }
                }
            }
            row[i  ] = vec[0];
            row[i+1] = vec[1];
            row[i+2] = mag;
            if (!hd_enabled && !isLastColumn) {
                row[i+3] = vec[0];
                row[i+4] = vec[1];
                row[i+5] = mag;
            }
        }
        rows[y] = row;
        if (!hd_enabled) {
            rows[y+1] = row;
        }
    }

    report.reset();
    progressHandle.set(0);  // signal that we are starting interpolation

    return new Promise((resolve, reject) => {

        (function batchInterpolate() {
            try {
                if (!cancel.requested) {
                    const start = Date.now();
                    while (y <= yMax) {
                        interpolateRow(y);
                        y += step;
                        if ((Date.now() - start) > MAX_TASK_TIME) {
                            // Interpolation is taking too long. Schedule the next batch for later and yield.
                            progressHandle.set((y - yMin + 1) / height);
                            setTimeout(batchInterpolate, MIN_SLEEP_TIME);
                            return;
                        }
                    }
                }
                resolve(createField(rows, !useFastOverlay ? mask : undefined, bounds));
            }
            catch (e) {
                reject(e);
            }
            progressHandle.set(1);  // 100% complete
            console.timeEnd("interpolating field");
            progressHandle.set(undefined);  // UNDONE: where to put this?
        })();

    });
}

export function defineFieldBuilder(
    fieldAgent,
    viewboxAgent,
    globeAgent,
    rendererAgent,
    primaryLayerAgent,
    activeLayerAgent,
    progressHandle,
    hd_enabled,
    interpolation_type,
    fastoverlayAgent,
) {

    function startInterpolation() {
        fieldAgent.submit(function() {
            return interpolateField(
                viewboxAgent,
                globeAgent,
                rendererAgent,
                primaryLayerAgent,
                activeLayerAgent,
                progressHandle,
                hd_enabled.get(),
                interpolation_type.get(),
                fastoverlayAgent,
                this.cancel,
            );
        });
    }

    rendererAgent.on($`render`, startInterpolation);
    primaryLayerAgent.on($`update`, startInterpolation);
    activeLayerAgent.on($`update`, startInterpolation);
    interpolation_type.on($`change`, startInterpolation);

    rendererAgent.on($`start redraw`, () => fieldAgent.cancel());
}
